import { useEffect, useMemo, useState } from 'react';

import { PREVIEW_INITIALIZED } from 'storybook/internal/core-events';
import { type API_IndexHash } from 'storybook/internal/types';

import {
  internal_checklistStore as checklistStore,
  internal_universalChecklistStore as universalChecklistStore,
} from '#manager-stores';
import { debounce, throttle } from 'es-toolkit/function';
import {
  type API,
  experimental_UniversalStore,
  experimental_useUniversalStore,
  useStorybookApi,
  useStorybookState,
} from 'storybook/manager-api';

import type { ItemState } from '../../../shared/checklist-store';
import type { ChecklistData } from '../../../shared/checklist-store/checklistData';
import { checklistData } from '../../../shared/checklist-store/checklistData';

type RawItemWithSection = ChecklistData['sections'][number]['items'][number] & {
  itemIndex: number;
  sectionId: string;
  sectionIndex: number;
  sectionTitle: string;
};

export type ChecklistItem = RawItemWithSection & {
  isAvailable: boolean;
  isOpen: boolean;
  isLockedBy: string | undefined;
  isImmutable: boolean;
  isReady: boolean;
  isCompleted: boolean;
  isAccepted: boolean;
  isDone: boolean;
  isSkipped: boolean;
  isMuted: boolean;
};

const subscriptions = new Map<string, void | (() => void)>();

const useStoryIndex = () => {
  const state = useStorybookState();
  const [index, setIndex] = useState<API_IndexHash | undefined>(() => state.index);
  const updateIndex = useMemo(() => throttle(setIndex, 500), []);

  useEffect(() => updateIndex(state.index), [state.index, updateIndex]);
  useEffect(() => () => updateIndex.cancel?.(), [updateIndex]);

  return index;
};

const checkAvailable = (
  item: RawItemWithSection,
  itemsById: Record<RawItemWithSection['id'], RawItemWithSection>,
  context: { api: API; index: API_IndexHash | undefined; item: RawItemWithSection }
) => {
  if (item.available && !item.available(context)) {
    return false;
  }
  for (const afterId of item.after ?? []) {
    if (itemsById[afterId] && !checkAvailable(itemsById[afterId], itemsById, context)) {
      return false;
    }
  }
  return true;
};

const checkSkipped = (
  item: RawItemWithSection,
  itemsById: Record<RawItemWithSection['id'], RawItemWithSection>,
  state: Record<string, ItemState>
) => {
  const itemValue = state[item.id];
  if (itemValue.status === 'skipped') {
    return true;
  }
  for (const afterId of item.after ?? []) {
    if (itemsById[afterId] && checkSkipped(itemsById[afterId], itemsById, state)) {
      return true;
    }
  }
  return false;
};

const getAncestorIds = (
  item: RawItemWithSection,
  itemsById: Record<RawItemWithSection['id'], RawItemWithSection>
): string[] => {
  if (!item.after || item.after.length === 0) {
    return [];
  }
  return item.after.flatMap((afterId) => {
    const afterItem = itemsById[afterId];
    return afterItem ? [...getAncestorIds(afterItem, itemsById), afterId] : [];
  });
};

const checkLockedBy = (
  item: RawItemWithSection,
  itemsById: Record<RawItemWithSection['id'], RawItemWithSection>,
  state: Record<string, ItemState>
): string | undefined =>
  getAncestorIds(item, itemsById).find(
    (id) => state[id].status !== 'accepted' && state[id].status !== 'done'
  );

export const useChecklist = () => {
  const api = useStorybookApi();
  const index = useStoryIndex();
  const [checklistState] = experimental_useUniversalStore(universalChecklistStore);
  const { loaded, items, widget } = checklistState;
  const { status } = universalChecklistStore;

  const [initialized, setInitialized] = useState(false);
  const [ready, setReady] = useState(false);

  const debounceReady = useMemo(() => debounce(() => setReady(true), 500), []);

  const itemsById = useMemo<Record<string, RawItemWithSection>>(() => {
    return Object.fromEntries(
      checklistData.sections.flatMap(
        ({ items, id: sectionId, title: sectionTitle }, sectionIndex) =>
          items.map(({ id, ...item }, itemIndex) => {
            return [id, { id, itemIndex, sectionId, sectionIndex, sectionTitle, ...item }];
          })
      )
    );
  }, []);

  const allItems = useMemo(() => {
    return Object.values(itemsById).map<ChecklistItem>((item) => {
      const { status, mutedAt } = items[item.id];
      const isOpen = status === 'open';
      const isAccepted = status === 'accepted';
      const isDone = status === 'done';
      const isCompleted = isAccepted || isDone;
      const isSkipped = !isCompleted && checkSkipped(item, itemsById, items);
      const isMuted = !!mutedAt || !!widget.disable;

      const isAvailable = isCompleted
        ? item.afterCompletion !== 'unavailable'
        : checkAvailable(item, itemsById, { api, index, item });
      const isLockedBy = checkLockedBy(item, itemsById, items);
      const isImmutable = isCompleted && item.afterCompletion === 'immutable';
      const isReady = isOpen && isAvailable && !isMuted && !isLockedBy;

      return {
        ...item,
        isAvailable,
        isOpen,
        isLockedBy,
        isImmutable,
        isReady,
        isCompleted,
        isAccepted,
        isDone,
        isSkipped,
        isMuted,
      };
    });
  }, [itemsById, items, widget, api, index]);

  const itemCollections = useMemo(() => {
    const availableItems = allItems.filter((item) => item.isAvailable);
    const openItems = availableItems.filter((item) => item.isOpen);
    const readyItems = openItems.filter((item) => item.isReady);

    // Collect a list of the next 3 tasks that are ready.
    // Tasks are pulled from each section in a round-robin fashion,
    // so that users can choose their own adventure.
    const nextItems = Object.values(
      readyItems.reduce<Record<string, ChecklistItem[]>>((acc, item) => {
        // Reset itemIndex to only include ready items.
        acc[item.sectionId] ??= [];
        acc[item.sectionId].push({ ...item, itemIndex: acc[item.sectionId].length });
        return acc;
      }, {})
    )
      .flat()
      .sort((a, b) => a.itemIndex - b.itemIndex)
      .slice(0, 3)
      .sort((a, b) => a.sectionIndex - b.sectionIndex);

    const progress = availableItems.length
      ? Math.round(((availableItems.length - openItems.length) / availableItems.length) * 100)
      : 100;

    return { availableItems, openItems, readyItems, nextItems, progress };
  }, [allItems]);

  useEffect(() => {
    if (!loaded || status !== experimental_UniversalStore.Status.READY) {
      return;
    }

    for (const item of allItems) {
      if (!item.subscribe) {
        continue;
      }

      const subscribed = subscriptions.has(item.id);
      if (item.isOpen && item.isAvailable && !subscribed) {
        subscriptions.set(
          item.id,
          item.subscribe({
            api,
            item,
            accept: () => checklistStore.accept(item.id),
            done: () => checklistStore.done(item.id),
            skip: () => checklistStore.skip(item.id),
          })
        );
      } else if (subscribed && !(item.isOpen && item.isAvailable)) {
        const unsubscribe = subscriptions.get(item.id);
        subscriptions.delete(item.id);
        if (typeof unsubscribe === 'function') {
          unsubscribe();
        }
      }
    }
  }, [api, loaded, status, allItems]);

  useEffect(() => {
    const initialize = () => setInitialized(true);
    const timeout = setTimeout(initialize, 1000);
    api.once(PREVIEW_INITIALIZED, initialize);
    return () => {
      clearTimeout(timeout);
      api.off(PREVIEW_INITIALIZED, initialize);
    };
  }, [api]);

  useEffect(() => {
    if (initialized && items && status === experimental_UniversalStore.Status.READY) {
      debounceReady();
    }
  }, [initialized, items, status, debounceReady]);

  return {
    ready,
    allItems,
    ...itemCollections,
    ...checklistStore,
    ...checklistState,
  };
};
